﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Collections;
using DynORM.ValueObjects;
using DynORM.Interfaces;
using DynORM.ReferenceObjects;
using DynORM.Interfaces.BLLMapper.Enum;
using DynORM.ConfigExtensions;
using DynORM.Enums;
using DynORM.Mappers;
using DynORM.TypeMappingSubsystem;
using DynORM.BLLMapper.Helpers;
using DynORM.ER.Mappers;

namespace DynORM.BLLMapper
{
    public class BLLAdaptiveMapper : IBLLAdaptiveMapper
    {
        private string connectionString;
        private DBMSTypes dbmsType;

        public BLLAdaptiveMapper(string connectionString, DBMSTypes dbmsType)
        {
            this.connectionString = connectionString;
            this.dbmsType = dbmsType;
        }

        public BLLAdaptiveMapper()
        {
            this.connectionString = DatabaseConfigSection.Settings.ConnectionString;
            string rdbmsKindStr = DatabaseConfigSection.Settings.DBMS;
            this.dbmsType = (DBMSTypes)Enum.Parse(typeof(DBMSTypes), rdbmsKindStr);
        }


        #region recursive methods and aux collections

        private Dictionary<IPrimaryKey, object> instanciatedObjects = new Dictionary<IPrimaryKey, object>();

        private void ReadingDeepInner(object parent, object bllObj)
        {
            //bllObj is now ghost
            object loaded;
            if (this.instanciatedObjects.TryGetValue(ObjectAndRowAux.GetInstance(dbmsType).GetPrimaryKeyFromObj(bllObj), out loaded) == false)
            {
                EntityRelation.Read(bllObj, this.connectionString, this.dbmsType); //now bllObj is loaded 
                this.instanciatedObjects.Add(ObjectAndRowAux.GetInstance(dbmsType).GetPrimaryKeyFromObj(bllObj), bllObj);
                foreach (object child in EntityRelation.GetChildren(bllObj)) //every child of bllObj is ghost
                    ReadingDeepInner(bllObj, child);
            }
            else
            {
                //if (parent != null) 
                //parent can be null only in the first recursion call, ta the moment this.instanciatedObjects has been Cleared so loaded == null 
                //and hense we won't get in this branch. that's why this check is redudant.
                ObjectAndRowAux.SubstituteChildInObj(parent, loaded, bllObj);
            }
        }

        private Dictionary<IPrimaryKey, object> storedEntities = new Dictionary<IPrimaryKey, object>();

        private void WritingDeepInner(object bllObj, bool updateStructure)
        {
            Entity.Store(bllObj, updateStructure, this.connectionString, this.dbmsType);
            this.storedEntities.Add(ObjectAndRowAux.GetInstance(dbmsType).GetPrimaryKeyFromObj(bllObj), bllObj);
            if (bllObj is BLLSuperType && LazyLoadStatus.IsGhostOrLoading(bllObj))
                return;
            EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);
            EntityRelation.Store(bllObj, this.connectionString, this.dbmsType);
            foreach (Tuple<int, object, RelationQuantity> t in EntityRelation.GetChildrenExtended(bllObj))
            {
                object obj;
                if (!this.storedEntities.TryGetValue(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(t.Item2), out obj))
                    WritingDeepInner(t.Item2, updateStructure);
            }
        }

        private Dictionary<IPrimaryKey, object> deletedObjects = new Dictionary<IPrimaryKey, object>();

        public void DeletingCascadeInner(object bllObj)
        {
            this.deletedObjects.Add(ObjectAndRowAux.GetInstance(dbmsType).GetPrimaryKeyFromObj(bllObj), bllObj);

            IRowDescriptor rd = TypeAndRowDescriptorAux.GetInstance(this.dbmsType).GetBLLObjTypeRowDescriptor(bllObj.GetType());
            if (RowDescriptorMapper.IsExistsInDB(rd, this.connectionString, this.dbmsType) == false || ObjectAndRowAux.GetInstance(this.dbmsType).IsObjExistsInDB(this.connectionString, bllObj) == false)
                return;

            IEnumerable<Tuple<int, object, RelationQuantity>> children = EntityRelation.GetChildsFromObjExcludeManyToManyExtended(bllObj); //exclude many-to-many to make it similar to DB delete cascade fk constraint's parameter
            object obj;
            foreach (Tuple<int, object, RelationQuantity> t in children)
                if (!this.deletedObjects.TryGetValue(ObjectAndRowAux.GetInstance(dbmsType).GetPrimaryKeyFromObj(t.Item2), out obj))
                    DeletingCascadeInner(t.Item2);

            if (ObjectAndRowAux.GetInstance(this.dbmsType).IsObjExistsInDB(this.connectionString, bllObj) == false) //in case of cycles in related object graph we could delete the object so it is neccessery  to recheck
                return;

            EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);
            Entity.Delete(bllObj, this.connectionString, this.dbmsType);
        }

        public void DeletingDeepInner(object bllObj)
        {
            this.deletedObjects.Add(ObjectAndRowAux.GetInstance(dbmsType).GetPrimaryKeyFromObj(bllObj), bllObj);

            IRowDescriptor rd = TypeAndRowDescriptorAux.GetInstance(this.dbmsType).GetBLLObjTypeRowDescriptor(bllObj.GetType());
            if (RowDescriptorMapper.IsExistsInDB(rd, this.connectionString, this.dbmsType) == false || ObjectAndRowAux.GetInstance(this.dbmsType).IsObjExistsInDB(this.connectionString, bllObj) == false)
                return;

            IEnumerable<Tuple<int, object, RelationQuantity>> children = EntityRelation.GetChildrenExtended(bllObj);
            object obj;
            foreach (Tuple<int, object, RelationQuantity> t in children)
                if (!this.deletedObjects.TryGetValue(ObjectAndRowAux.GetInstance(dbmsType).GetPrimaryKeyFromObj(t.Item2), out obj))
                    DeletingDeepInner(t.Item2);

            if (ObjectAndRowAux.GetInstance(this.dbmsType).IsObjExistsInDB(this.connectionString, bllObj) == false) //in case of cycles in related object graph we could delete the object so it is neccessery  to recheck
                return;

            Entity.Delete(bllObj, this.connectionString, this.dbmsType);
        }

        #endregion

        #region aux for topological sort

        private IEnumerable<object> GetChildren(object bllObj, IEnumerable<object> listToConsider)
        {
            List<object> result = new List<object>();

            IEnumerable<object> children = EntityRelation.GetChildren(bllObj);
            foreach (object child in children)
                foreach (object item in listToConsider)
                    if (ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(child).Equals(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(item)))
                        result.Add(item);

            return result;
        }

        #endregion

        #region generic public interface

        public T ReadDeep<T>(params object[] bllObjIDs)
        {
            Type bllObjType = typeof(T);
            if (bllObjType == null)
                throw new NullReferenceException("ReadDeep method's parameter 'bllObjType' cannot be null");

            T result;

            this.instanciatedObjects.Clear();
            result = (T)Entity.Read(bllObjType, this.connectionString, this.dbmsType, bllObjIDs);
            if (result != null) //there is no record in db with the provided id
                ReadingDeepInner(null, result);

            return result;
        }

        public T Read<T>(params object[] bllObjIDs)
        {
            Type bllObjType = typeof(T);
            if (bllObjType == null)
                throw new NullReferenceException("Read method's parameter 'bllObjType' cannot be null");

            T result;
            if (typeof(BLLSuperType).IsAssignableFrom(bllObjType))
            {
                result = (T)Entity.Read(bllObjType, this.connectionString, this.dbmsType, bllObjIDs);
                if (result != null) //there is no record in db with the provided id
                    EntityRelation.Read(result, this.connectionString, this.dbmsType); //now result is loaded (its children are ghosts)
            }
            else //for POCOs full graph
            {
                result = ReadDeep<T>(bllObjIDs);
            }
            return result;
        }

        public IEnumerable<T> Read<T>()
        {
            Type bllObjType = typeof(T);
            if (bllObjType == null)
                throw new NullReferenceException("Read method's parameter 'bllObjType' cannot be null");

            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Reading full graph of related objects for all objects of this type is not implemented. It may cause huge overhead. Use Read(Type bllObjType, params object[] bllObjIDs) instead.");

            this.instanciatedObjects.Clear();
            IEnumerable<T> result = Entity.Read(bllObjType, this.connectionString, this.dbmsType).Cast<T>();
            foreach (T obj in result)
            {
                EntityRelation.Read(obj, this.connectionString, this.dbmsType); //now obj is loaded (its children are ghosts)
                foreach (Tuple<int, object, RelationQuantity> t in EntityRelation.GetChildrenExtended(obj)) //every child of bllObj is ghost
                {
                    //in order to have the same identity (same reference) of objects with equal primary keys substitute those that differ
                    object newobj;
                    if (this.instanciatedObjects.TryGetValue(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(t.Item2), out newobj))
                        ObjectAndRowAux.SubstituteChildInObj(obj, newobj, t.Item2);
                    else
                        this.instanciatedObjects.Add(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(t.Item2), t.Item2);
                }
            }
            return result;
        }

        public IEnumerable<T> Read<T>(IEnumerable<BLLComparison> predicate)
        {
            Type bllObjType = typeof(T);
            if (bllObjType == null)
                throw new NullReferenceException("Read method's parameter 'bllObjType' cannot be null");


            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Reading full graph of related objects for all objects of this type is not implemented. It may cause huge overhead. Use Read(Type bllObjType, params object[] bllObjIDs) instead.");

            this.instanciatedObjects.Clear();
            IEnumerable<T> result = Entity.Read(bllObjType, this.connectionString, this.dbmsType, predicate).Cast<T>();
            foreach (T obj in result)
            {
                EntityRelation.Read(obj, this.connectionString, this.dbmsType); //now obj is loaded (its children are ghosts)
                foreach (Tuple<int, object, RelationQuantity> t in EntityRelation.GetChildrenExtended(obj)) //every child of bllObj is ghost
                {
                    //in order to have the same identity (same reference) of objects with equal primary keys substitute those that differ
                    object newobj;
                    if (this.instanciatedObjects.TryGetValue(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(t.Item2), out newobj))
                        ObjectAndRowAux.SubstituteChildInObj(obj, newobj, t.Item2);
                    else
                        this.instanciatedObjects.Add(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(t.Item2), t.Item2);
                }
            }
            return result;
        }

        public T ReadLazy<T>(params object[] bllObjIDs)
        {
            Type bllObjType = typeof(T);
            if (bllObjType == null)
                throw new NullReferenceException("ReadLazy method's parameter 'bllObjType' cannot be null");

            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Lazy loading can't be applied to POCO. Object should be of BLLSuperType-derived class.");
            
            T result = (T)Entity.Read(bllObjType, this.connectionString, this.dbmsType, bllObjIDs); //result is ghost now
            return result;
        }

        public IEnumerable<T> ReadLazy<T>()
        {
            Type bllObjType = typeof(T);
            if (bllObjType == null)
                throw new NullReferenceException("ReadLazy method's parameter 'bllObjType' cannot be null");

            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Lazy loading can't be applied to POCO. Object should be of BLLSuperType-derived class.");

            IEnumerable<T> result = Entity.Read(bllObjType, this.connectionString, this.dbmsType).Cast<T>(); //result is ghost now
            return result;
        }

        public IEnumerable<T> ReadLazy<T>(IEnumerable<BLLComparison> predicate)
        {
            Type bllObjType = typeof(T);
            if (bllObjType == null)
                throw new NullReferenceException("ReadLazy method's parameter 'bllObjType' cannot be null");

            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Lazy loading can't be applied to POCO. Object should be of BLLSuperType-derived class.");

            IEnumerable<T> result = Entity.Read(bllObjType, this.connectionString, this.dbmsType, predicate).Cast<T>(); //result is ghost now
            return result;
        }

        public IDictionary<IPrimaryKey, object> UploadLazyDeep<T>(T bllObj)
        {
            if (bllObj == null)
                throw new NullReferenceException("UploadLazy method's parameter 'bllObj' cannot be null");

            if (!(bllObj is BLLSuperType))
                throw new Exception("Lazy loading logic can be applied only to BLLSuperType-derived class object.");

            if (LazyLoadStatus.IsLoaded(bllObj) || LazyLoadStatus.IsLoading(bllObj)) //otherwise can fall into endless loop
                return new Dictionary<IPrimaryKey, object>();

            this.instanciatedObjects.Clear();
            ReadingDeepInner(null, bllObj);

            return this.instanciatedObjects;
        }

        public IEnumerable UploadLazy<T>(T bllObj)
        {
            if (bllObj == null)
                throw new NullReferenceException("UploadLazy method's parameter 'bllObj' cannot be null");

            if (!(bllObj is BLLSuperType))
                throw new Exception("Lazy loading logic can be applied only to BLLSuperType-derived class object.");

            if (LazyLoadStatus.IsLoaded(bllObj) || LazyLoadStatus.IsLoading(bllObj)) //otherwise can fall into endless loop
                return null;

            EntityRelation.Read(bllObj, this.connectionString, this.dbmsType); //now result is loaded (its children are ghosts)
            return EntityRelation.GetChildren(bllObj);
        }

        public void SubstituteChildInObj<T1, T2>(T1 parent, T2 newchild, T2 oldchild)
        {
            ObjectAndRowAux.SubstituteChildInObj(parent, newchild, oldchild);
        }

        public void WriteDeep<T>(T bllObj, bool updateStructure)
        {
            if (bllObj == null)
                throw new NullReferenceException("WriteDeep method's parameter 'bllObj' cannot be null");

            this.storedEntities.Clear();
            WritingDeepInner(bllObj, updateStructure);
        }

        public void Write<T>(T bllObj, bool updateStructure)
        {
            if (bllObj == null)
                throw new NullReferenceException("Write method's parameter 'bllObj' cannot be null");

            Entity.Store(bllObj, updateStructure, this.connectionString, this.dbmsType);
            //if (bllObj is BLLSuperType && LazyLoadStatus.IsGhostOrLoading(bllObj))
            //    return;
            EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);
            EntityRelation.Store(bllObj, this.connectionString, this.dbmsType);
        }

        //public void WriteBatch(object[] bllObjs, bool updateStructure)
        //{
        //    if (bllObjs == null)
        //        throw new NullReferenceException("Write method's parameter 'bllObjs' cannot be null");

        //    if (bllObjs.Count() == 0)
        //        return;

        //    //it's important first to store all the "entities" in db and only then add "relation" between them (values in foreign keys fields)
        //    //because some objects may not even exists in db yet
        //    foreach (object bllObj in bllObjs)
        //        Entity.Store(bllObj, updateStructure, this.connectionString, this.dbmsType);

        //    foreach (object bllObj in bllObjs)
        //    {
        //        EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);
        //        EntityRelation.Store(bllObj, this.connectionString, this.dbmsType);
        //    }
        //}


        public void UpdateStructure<T>()
        {
            Type bllObjType = typeof(T);
            Entity.StoreStructure(bllObjType, this.connectionString, this.dbmsType);
        }


        public void Delete<T>(T bllObj)
        {
            if (bllObj == null)
                throw new NullReferenceException("Delete method's parameter 'bllObj' cannot be null");

            EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);
            Entity.Delete(bllObj, this.connectionString, this.dbmsType);
        }

        public void Delete<T>(params object[] bllObjIDs)
        {
            Type bllObjType = typeof(T);
            if (bllObjType == null)
                throw new NullReferenceException("Delete method's parameter 'bllObjType' cannot be null");

            T bllObj = Read<T>(bllObjIDs);
            if (bllObj != null)
                Delete(bllObj);
        }

        //public void DeleteBatch(object[] bllObjs)
        //{
        //    if (bllObjs == null)
        //        throw new NullReferenceException("Write method's parameter 'bllObjs' cannot be null");

        //    if (bllObjs.Count() == 0)
        //        return;

        //    //it's important first to reset all the "relation" in db and only then delete "entities" between them (values in foreign keys fields)
        //    //when we reset object should exist in db otherwise an error occure
        //    foreach (object bllObj in bllObjs)
        //        EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);

        //    foreach (object bllObj in bllObjs)
        //        Entity.Delete(bllObj, connectionString, dbmsType);
        //}

        public void DeleteCascade<T>(T bllObj)
        {
            if (bllObj == null)
                throw new NullReferenceException("Delete method's parameter 'bllObj' cannot be null");

            this.deletedObjects.Clear();
            DeletingCascadeInner(bllObj);
        }

        public void DeleteCascade<T>(params object[] bllObjIDs)
        {
            Type bllObjType = typeof(T);
            T bllObj = Read<T>(bllObjIDs);
            DeleteCascade(bllObj);
        }


        public T Create<T>(params object[] bllObjIDs)
        {
            var bllObjType = typeof(T);
            if (bllObjType == null)
                throw new NullReferenceException("Create method's parameter 'bllObjType' cannot be null");

            T result;

            bool pkctor = false;
            foreach (ConstructorInfo ctor in bllObjType.GetConstructors())
                if (ctor.GetCustomAttributes(typeof(PrimaryConstructorAttribute), true).Count() == 1)
                    pkctor = true;

            if (pkctor)
            {
                result = (T)Activator.CreateInstance(bllObjType, bllObjIDs);
            }
            else
            {
                result = (T)Activator.CreateInstance(bllObjType);
                IDictionary<int, MemberInfo> idmis = TypeAndRowDescriptorAux.GetBLLObjTypePrimaryKeyMemberInfos(bllObjType);
                foreach (KeyValuePair<int, MemberInfo> kv in idmis)
                    if (kv.Value is FieldInfo)
                        ((FieldInfo)kv.Value).SetValue(result, bllObjIDs[kv.Key]);
                    else if (kv.Value is PropertyInfo)
                        ((PropertyInfo)kv.Value).SetValue(result, bllObjIDs[kv.Key], null);
                    else
                        throw new Exception("Primary key member should be either field or property.");
            }
            return result;
        }

        #endregion

        #region public interface

        public object ReadDeep(Type bllObjType, params object[] bllObjIDs)
        {
            if (bllObjType == null)
                throw new NullReferenceException("ReadDeep method's parameter 'bllObjType' cannot be null");

            object result;

            this.instanciatedObjects.Clear();
            result = Entity.Read(bllObjType, this.connectionString, this.dbmsType, bllObjIDs);
            if (result != null) //there is no record in db with the provided id
                ReadingDeepInner(null, result);

            return result;
        }

        public object Read(Type bllObjType, params object[] bllObjIDs)
        {
            if (bllObjType == null)
                throw new NullReferenceException("Read method's parameter 'bllObjType' cannot be null");

            object result;
            if (typeof(BLLSuperType).IsAssignableFrom(bllObjType))
            {
                result = Entity.Read(bllObjType, this.connectionString, this.dbmsType, bllObjIDs);
                if (result != null) //there is no record in db with the provided id
                    EntityRelation.Read(result, this.connectionString, this.dbmsType); //now result is loaded (its children are ghosts)
            }
            else //for POCOs full graph
            {
                result = ReadDeep(bllObjType, bllObjIDs);
            }
            return result;
        }

        public IEnumerable<object> Read(Type bllObjType)
        {
            if (bllObjType == null)
                throw new NullReferenceException("Read method's parameter 'bllObjType' cannot be null");

            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Reading full graph of related objects for all objects of this type is not implemented. It may cause huge overhead. Use Read(Type bllObjType, params object[] bllObjIDs) instead.");

            this.instanciatedObjects.Clear();
            IEnumerable<object> result = Entity.Read(bllObjType, this.connectionString, this.dbmsType);
            foreach (object obj in result)
            {
                EntityRelation.Read(obj, this.connectionString, this.dbmsType); //now obj is loaded (its children are ghosts)
                foreach (Tuple<int, object, RelationQuantity> t in EntityRelation.GetChildrenExtended(obj)) //every child of bllObj is ghost
                {
                    //in order to have the same identity (same reference) of objects with equal primary keys substitute those that differ
                    object newobj;
                    if (this.instanciatedObjects.TryGetValue(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(t.Item2), out newobj))
                        ObjectAndRowAux.SubstituteChildInObj(obj, newobj, t.Item2);
                    else
                        this.instanciatedObjects.Add(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(t.Item2), t.Item2);
                }
            }
            return result;
        }

        public IEnumerable<object> Read(Type bllObjType, IEnumerable<BLLComparison> predicate)
        {
            if (bllObjType == null)
                throw new NullReferenceException("Read method's parameter 'bllObjType' cannot be null");


            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Reading full graph of related objects for all objects of this type is not implemented. It may cause huge overhead. Use Read(Type bllObjType, params object[] bllObjIDs) instead.");

            this.instanciatedObjects.Clear();
            IEnumerable<object> result = Entity.Read(bllObjType, this.connectionString, this.dbmsType, predicate);
            foreach (object obj in result)
            {
                EntityRelation.Read(obj, this.connectionString, this.dbmsType); //now obj is loaded (its children are ghosts)
                foreach (Tuple<int, object, RelationQuantity> t in EntityRelation.GetChildrenExtended(obj)) //every child of bllObj is ghost
                {
                    //in order to have the same identity (same reference) of objects with equal primary keys substitute those that differ
                    object newobj;
                    if (this.instanciatedObjects.TryGetValue(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(t.Item2), out newobj))
                        ObjectAndRowAux.SubstituteChildInObj(obj, newobj, t.Item2);
                    else
                        this.instanciatedObjects.Add(ObjectAndRowAux.GetInstance(this.dbmsType).GetPrimaryKeyFromObj(t.Item2), t.Item2);
                }
            }
            return result;
        }

        public object ReadLazy(Type bllObjType, params object[] bllObjIDs)
        {
            if (bllObjType == null)
                throw new NullReferenceException("ReadLazy method's parameter 'bllObjType' cannot be null");

            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Lazy loading can't be applied to POCO. Object should be of BLLSuperType-derived class.");

            object result = Entity.Read(bllObjType, this.connectionString, this.dbmsType, bllObjIDs); //result is ghost now
            return result;
        }

        public IEnumerable<object> ReadLazy(Type bllObjType)
        {
            if (bllObjType == null)
                throw new NullReferenceException("ReadLazy method's parameter 'bllObjType' cannot be null");

            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Lazy loading can't be applied to POCO. Object should be of BLLSuperType-derived class.");

            IEnumerable<object> result = Entity.Read(bllObjType, this.connectionString, this.dbmsType); //result is ghost now
            return result;
        }

        public IEnumerable<object> ReadLazy(Type bllObjType, IEnumerable<BLLComparison> predicate)
        {
            if (bllObjType == null)
                throw new NullReferenceException("ReadLazy method's parameter 'bllObjType' cannot be null");

            if (!typeof(BLLSuperType).IsAssignableFrom(bllObjType))
                throw new Exception("Lazy loading can't be applied to POCO. Object should be of BLLSuperType-derived class.");

            IEnumerable<object> result = Entity.Read(bllObjType, this.connectionString, this.dbmsType, predicate); //result is ghost now
            return result;
        }

        public IDictionary<IPrimaryKey, object> UploadLazyDeep(object bllObj)
        {
            if (bllObj == null)
                throw new NullReferenceException("UploadLazy method's parameter 'bllObj' cannot be null");

            if (!(bllObj is BLLSuperType))
                throw new Exception("Lazy loading logic can be applied only to BLLSuperType-derived class object.");

            if (LazyLoadStatus.IsLoaded(bllObj) || LazyLoadStatus.IsLoading(bllObj)) //otherwise can fall into endless loop
                return new Dictionary<IPrimaryKey, object>();

            this.instanciatedObjects.Clear();
            ReadingDeepInner(null, bllObj);

            return this.instanciatedObjects;
        }

        public IEnumerable UploadLazy(object bllObj)
        {
            if (bllObj == null)
                throw new NullReferenceException("UploadLazy method's parameter 'bllObj' cannot be null");

            if (!(bllObj is BLLSuperType))
                throw new Exception("Lazy loading logic can be applied only to BLLSuperType-derived class object.");

            if (LazyLoadStatus.IsLoaded(bllObj) || LazyLoadStatus.IsLoading(bllObj)) //otherwise can fall into endless loop
                return null;

            EntityRelation.Read(bllObj, this.connectionString, this.dbmsType); //now result is loaded (its children are ghosts)
            return EntityRelation.GetChildren(bllObj);
        }

        public void SubstituteChildInObj(object parent, object newchild, object oldchild)
        {
            ObjectAndRowAux.SubstituteChildInObj(parent, newchild, oldchild);
        }


        public void WriteDeep(object bllObj, bool updateStructure)
        {
            if (bllObj == null)
                throw new NullReferenceException("WriteDeep method's parameter 'bllObj' cannot be null");

            this.storedEntities.Clear();
            WritingDeepInner(bllObj, updateStructure);
        }

        public void Write(object bllObj, bool updateStructure)
        {
            if (bllObj == null)
                throw new NullReferenceException("Write method's parameter 'bllObj' cannot be null");

            Entity.Store(bllObj, updateStructure, this.connectionString, this.dbmsType);
            //if (bllObj is BLLSuperType && LazyLoadStatus.IsGhostOrLoading(bllObj))
            //    return;
            EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);
            EntityRelation.Store(bllObj, this.connectionString, this.dbmsType);
        }

        public void WriteBatch(object[] bllObjs, bool updateStructure)
        {
            if (bllObjs == null)
                throw new NullReferenceException("Write method's parameter 'bllObjs' cannot be null");

            if (bllObjs.Count() == 0)
                return;

            //it's important first to store all the "entities" in db and only then add "relation" between them (values in foreign keys fields)
            //because some objects may not even exists in db yet
            HashSet<string> checkedTypes = new HashSet<string>();
            foreach (object bllObj in bllObjs)
            {
                DetermineNecessityStructureCheck(checkedTypes, bllObj.GetType().FullName, out updateStructure);
                Entity.Store(bllObj, updateStructure, this.connectionString, this.dbmsType);
            }
            checkedTypes.Clear();


            foreach (object bllObj in bllObjs)
            {
                EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);
                EntityRelation.Store(bllObj, this.connectionString, this.dbmsType);
            }
        }

        private static void DetermineNecessityStructureCheck(HashSet<string> checkedTypes, string fullType, out bool updateStructure)
        {
            if (checkedTypes.Contains(fullType))
            {
                updateStructure = false;
            }
            else
            {
                checkedTypes.Add(fullType);
                updateStructure = true;
            }

        }



        public void UpdateStructure(Type bllObjType)
        {
            if (bllObjType == null)
                throw new NullReferenceException("UpdateStructure method's parameter 'bllObjType' cannot be null");

            Entity.StoreStructure(bllObjType, this.connectionString, this.dbmsType);
        }



        public void Delete(object bllObj)
        {
            if (bllObj == null)
                throw new NullReferenceException("Delete method's parameter 'bllObj' cannot be null");

            EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);
            Entity.Delete(bllObj, this.connectionString, this.dbmsType);
        }

        public void Delete(Type bllObjType, params object[] bllObjIDs)
        {
            if (bllObjType == null)
                throw new NullReferenceException("Delete method's parameter 'bllObjType' cannot be null");

            object bllObj = Read(bllObjType, bllObjIDs);
            if (bllObj != null)
                Delete(bllObj);
        }

        public void DeleteBatch(object[] bllObjs)
        {
            if (bllObjs == null)
                throw new NullReferenceException("Write method's parameter 'bllObjs' cannot be null");

            if (bllObjs.Count() == 0)
                return;

            //it's important first to reset all the "relation" in db and only then delete "entities" between them (values in foreign keys fields)
            //when we reset object should exist in db otherwise an error occure
            foreach (object bllObj in bllObjs)
                EntityRelation.ResetAll(bllObj, this.connectionString, this.dbmsType);

            foreach (object bllObj in bllObjs)
                Entity.Delete(bllObj, connectionString, dbmsType);
        }

        public void DeleteCascade(object bllObj)
        {
            if (bllObj == null)
                throw new NullReferenceException("Delete method's parameter 'bllObj' cannot be null");

            this.deletedObjects.Clear();
            DeletingCascadeInner(bllObj);
        }

        public void DeleteCascade(Type bllObjType, params object[] bllObjIDs)
        {
            object bllObj = Read(bllObjType, bllObjIDs);
            DeleteCascade(bllObj);
        }


        public object Create(Type bllObjType, params object[] bllObjIDs)
        {
            if (bllObjType == null)
                throw new NullReferenceException("Create method's parameter 'bllObjType' cannot be null");

            object result;

            bool pkctor = false;
            foreach (ConstructorInfo ctor in bllObjType.GetConstructors())
                if (ctor.GetCustomAttributes(typeof(PrimaryConstructorAttribute), true).Count() == 1)
                    pkctor = true;

            if (pkctor)
            {
                result = Activator.CreateInstance(bllObjType, bllObjIDs);
            }
            else
            {
                result = Activator.CreateInstance(bllObjType);
                IDictionary<int, MemberInfo> idmis = TypeAndRowDescriptorAux.GetBLLObjTypePrimaryKeyMemberInfos(bllObjType);
                foreach (KeyValuePair<int, MemberInfo> kv in idmis)
                    if (kv.Value is FieldInfo)
                        ((FieldInfo)kv.Value).SetValue(result, bllObjIDs[kv.Key]);
                    else if (kv.Value is PropertyInfo)
                        ((PropertyInfo)kv.Value).SetValue(result, bllObjIDs[kv.Key], null);
                    else
                        throw new Exception("Primary key member should be either field or property.");
            }
            return result;
        }

        #endregion
    
    }
}
